package stackOverflow;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;

/**
 * @author Boris Cherkasky
 *
 */
public class JsonHandler {
    
    //DEBUG flag
    private static final boolean DEBUG = false;
    //json reader object
    private JsonReader jReader;
    
    public JsonHandler(InputStream inputStream) throws UnsupportedEncodingException, IOException {
	jReader = new JsonReader(new InputStreamReader(new GZIPInputStream(inputStream), "utf-8"));
	jReader.setLenient(true);
    }
    
    /**
     * Get the total number of result objects in the json response
     * @return
     */
    public int getNumberOfResultObjects() {
	int number = -1;
	moveToName("total");
	try {
	    if (!isEndOfDocument()){
	    	number = jReader.nextInt();
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}
	return number;
    }
    
    /**
     * parse search results and get answers id's
     */
    public ArrayList<CodeObject> searchResult() {
	ArrayList<CodeObject> questions = new ArrayList<CodeObject>();
	CodeObject question;
	try {
	    //NOTE - the order between the moveToName(name) calls are VERY IMPORTANT
	    //and depends on the json response structure!
	    
	    while (!isEndOfDocument()){
		question = new CodeObject();
		//get question id
		moveToName("question_id");
	        if (!isEndOfDocument()){
	            question.setId(jReader.nextInt());
	        }
	        
	        //get user display name and reputation
	        extractUserDetails(question);
	        //get post meta data
	        extractMeta(question);
	        if (question.getId() != 0){
	            questions.add(question);
	        }
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}
	return questions;
    }
    /**
     * extract from json creation date, last activity date, up and down votes count, score
     * 
     * @param codeObj
     * @throws IOException
     */
    private void extractMeta(CodeObject codeObj) throws IOException {
	//get creation date
	moveToName("creation_date");
	if (!isEndOfDocument()){
	    codeObj.setCreationDate(jReader.nextLong());
	}
	//get last activity date
	moveToName("last_activity_date");
	if (!isEndOfDocument()){
	    codeObj.setLastActivityDate(jReader.nextLong());
	}
	//get up vote count
	moveToName("up_vote_count");
	if (!isEndOfDocument()){
	    codeObj.setUpVoteCount(jReader.nextInt());
	}
	//get down vote count
	moveToName("down_vote_count");
	if (!isEndOfDocument()){
	    codeObj.setDownVoteCount(jReader.nextInt());
	}
	//get question view count
	moveToName("view_count");
	if (!isEndOfDocument()){
	    codeObj.setViewCount(jReader.nextInt());
//	    System.out.println("Views: " + codeObj.getViewCount());
	}
	//get question score
	moveToName("score");
	if (!isEndOfDocument()){
	    codeObj.setScore(jReader.nextInt());
	}
    }
    
    /**
     * extract question body from json question query response
     * @return String - question body
     */
    public String questionCodeSamples() {
	moveToName("body");
	try {
	    if (!isEndOfDocument()){
	        return jReader.nextString();
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}
	return null;
    }
    
    /**
     * extract AnswerObject from json
     * NOTE - AnswerObject.code will have one entry which is the WHOLE answer body
     * need to process the answer body afterwards and update AnswerObject.code attribute accordingly 
     * @return
     */
    public ArrayList<AnswerObject> questionAnswers() {
	ArrayList<AnswerObject> answers = new ArrayList<AnswerObject>();
	try {
	    //iterate over the document and extract answers
	    while (!isEndOfDocument()){
		//NOTE - the order between the moveToName(name) calls are VERY IMPORTANT
		//and depends on the json response structure!
		
		AnswerObject answer = new AnswerObject();
		//get answer id
	        moveToName("answer_id");
	        if (!isEndOfDocument()){
	            answer.setId(jReader.nextInt());
	        }
		//get is accepted flag
	        moveToName("accepted");
	        if (!isEndOfDocument()){
	            answer.setAccepted(jReader.nextBoolean());
	        }
	        //get user display name and reputation 
	        extractUserDetails(answer);
	        //get post meta
	        extractMeta(answer);
	        //get body
		moveToName("body");
	        if (!isEndOfDocument()){
	            answer.setCode(jReader.nextString());
	        }
	        
	        //if found new answer, add it 
	        if (answer.getId() != 0){
	            answers.add(answer);
	        }
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}

	return answers;
    }
    
    /**
     * @return boolean - true iff reached end of json document
     * @throws IOException
     */
    private boolean isEndOfDocument() throws IOException {
	return jReader.peek().equals(JsonToken.END_DOCUMENT);
    }
    
    /**
     * moves the reader stream to the wanted token name
     * NOTE: always check after function returnes that the 
     * jReaded.peek() does not return JsonToken.END_DOCUMENT
     * @param name
     */
    private void moveToName(String name){
	try {
	    while (!consumeObject(name)){
		consumeObjectOrArrayDelimiters();
	    }
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }
    
    /**
     * consumes object until {name} token is found or object ends
     * @param name
     * @throws IOException
     * @return true if halted when token found (and not object end) or document ended
     */
    private boolean consumeObject(String name) throws IOException{
	while (!isEndOfDocument()){
	    if (consumeObjectOrArrayDelimiters()){
		continue;
	    }
	    if (jReader.peek().equals(JsonToken.NAME)){
		String tokenName = jReader.nextName();
	        if (tokenName.equals(name)){
	            //found the correct token
	            return true;
	        } else {
	            if (JsonHandler.DEBUG){
	        	System.out.println(tokenName);
	            }
	            if (jReader.peek().equals(JsonToken.BEGIN_ARRAY) || 
	        	    jReader.peek().equals(JsonToken.BEGIN_OBJECT)){
	        	//if next token is an object or an array - consume it
	        	continue;
	            }
	            jReader.skipValue();
	        }
	    } else {
//		System.out.println(jReader.peek().toString());
	        jReader.skipValue();
	    }	    
	}
	
	if (isEndOfDocument()){
	    //reached the end of the document
	    return true;
	}
	return false;
    }
    
    /**
     * consume array and object begin/end
     * 
     * @return
     * @throws IOException
     */
    private boolean consumeObjectOrArrayDelimiters() throws IOException {
	if (jReader.peek().equals(JsonToken.BEGIN_ARRAY)){
	    jReader.beginArray();
	    return true;
	}
	if (jReader.peek().equals(JsonToken.END_ARRAY)){
	    jReader.endArray();
	    return true;
	}
	if (jReader.peek().equals(JsonToken.BEGIN_OBJECT)){
	    jReader.beginObject();
	    return true;
	}
	if (jReader.peek().equals(JsonToken.END_OBJECT)){
	    jReader.endObject();
	    return true;
	}
	return false;
    }
    
    /**
     * extract user display name and reputation from json
     * 
     * @param codeObj
     * @throws IOException
     */
    private void extractUserDetails(CodeObject codeObj) throws IOException {
	//get user display name
	moveToName("display_name");
	if (!isEndOfDocument()){
	    codeObj.setUserDisplayName(jReader.nextString());
	}
	//get user reputation
	moveToName("reputation");
	if (!isEndOfDocument()){
	    codeObj.setUserRep(jReader.nextInt());
	}
    }
}
